package com.ari.eft.transport.tls;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.util.logging.Logger;

import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;

/**
 * RFC 3734 specifies a transport mapping for EPP over TCP; this class
 * implements that mapping.  That specification requires that the session is
 * layered over TLS (Transport Layer Security).  This class complies with RFC
 * 3734 in implementing the Session interface.  It also implements its own
 * StatsManager since it is tightly coupled with the Session implementation.
 */
public class TLSSession implements EPPTLSTransport {
    private static final int BUF_SIZE = 4096;
    private java.io.DataInputStream in;
    private java.io.DataOutputStream out;
    private TLSContext ctx;
    private SSLSocket socket;

    private boolean inUse;
    private boolean isOpen;
    private boolean isInvalid;
    private long acquireTimeout;

    private InetAddress inaddr;
    private int port;
    private int soTimeout;
    private String username;
    private String password;
    private String eppVersion;
    private String language;
    private String[] objURIs, extURIs;

    private String pname;

    private Logger debugLogger;
    private Logger supportLogger;
    private Logger userLogger;

    {
        pname = TLSSession.class.getPackage().getName();
        debugLogger = Logger.getLogger(pname + ".debug");
        supportLogger = Logger.getLogger(pname + ".support");
        userLogger = Logger.getLogger(pname + ".user");
        isInvalid = true;
    }

    public TLSSession() {
    }

    public TLSSession(SessionProperties props) throws SessionConfigurationException {
        configure(props);
    }

    /**
     * Configure the session as described in the Session interface.
     *
     * @throws SessionConfigurationException Possible causes:
     * <ul>
     * <li>KeyStoreNotFoundException</li>
     * <li>KeyStoreTypeException</li>
     * <li>NoSuchAlgorithmException</li>
     * <li>UnrecoverableKeyException</li>
     * <li>CertificateException</li>
     * <li>KeyStoreReadException</li>
     * <li>UnknownHostException</li>
     * <li>FileNotFoundException</li>
     * </ul>
     */
    public void configure(SessionProperties properties)
            throws SessionConfigurationException {

        this.port = properties.getPort();
        this.username = properties.getClientID();
        this.password = properties.getClientPW();
        this.eppVersion = properties.getVersion();
        this.language = properties.getLanguage();
        this.objURIs = properties.getObjURIs();
        this.extURIs = properties.getExtURIs();
        // commandCounter = new CommandCounter(
        // properties.getCommandLimitInterval());
        this.acquireTimeout = properties.getAcquireTimeout();
        this.soTimeout = properties.getSocketTimeout();

        try {
            inaddr = InetAddress.getByName(properties.getHostname());

            if (ctx == null) {
                ctx = new TLSContext(properties.getKeyStoreFilename(),
                        properties.getKeyStorePassphrase(),
			properties.getTrustStoreFilename(),
			properties.getTrustStorePassphrase(),
                        properties.getKeyStoreType(),
                        properties.getSSLAlgorithm());
            }
        } catch (Exception e) {
            throw new SessionConfigurationException(e);
        }
    }

    public boolean isInvalid() {
        return isInvalid;
    }

    public boolean isOpen() {
        return isOpen;
    }

    /**
     * Implements the open method in the Session interface.  Please refer to
     * the Session documentation for a description of the open contract.
     *
     * @throws SessionOpenException The getCause() method should be invoked on
     * the exception thrown.  The cause may be one of:
     * <dl>
     * <dt>SSLHandshakeException</dt><dd>The SSL handshake failed.  The reason
     * is described in the exception message, and is also recorded in the user
     * logs.</dd>
     * <dt>IOException</dt><dd>See the log record published to the user logs,
     * or check the exception message.</dd>
     * <dt>GreetingException</dt><dd>The service element received from the
     * server upon connection establishment was not a valid EPP greeting.</dd>
     * <dt>LoginException</dt><dd>The login command failed to establish an EPP
     * session.  The cause, available via getCause(), describes the specific
     * reason for failure.</dd>
     * </dl>
     */
    public void open() throws SessionOpenException {
        isInvalid = true;
        try {
            openSocket();
        } catch (SSLHandshakeException e) {
            // userLogger.severe(ErrorPkg.getMessage("TLSContext.createSocket.0",
            // "<<java.home>>", System.getProperty(
            // "java.home", "java.home")));
            // userLogger.severe(e.getMessage());
            throw new SessionOpenException(e);
        } catch (ConnectException e) {
            // final String errorMessage =
            // ErrorPkg.getMessage("net.socket.open.fail", new String[] {
            // "<<port>>",
            // "<<host>>" }, new String[] { String.valueOf(port),
            // inaddr.getHostAddress() });
            // userLogger.severe(errorMessage);
            // userLogger.severe(e.getMessage());
            throw new SessionOpenException("Some message");
        } catch (IOException e) {
            // userLogger.severe(ErrorPkg.getMessage("net.socket.open.fail", new
            // String[] { "<<port>>", "<<host>>" },
            // new String[] { String.valueOf(port), inaddr.getHostAddress() }));
            // userLogger.severe(e.getMessage());
            throw new SessionOpenException(e.getCause());
        }
    }

    public void changePassword(String newPassword) throws SessionOpenException {
        open();
        close();
    }

    private void openSocket() throws SSLHandshakeException, IOException {
        socket = ctx.createSocket(inaddr.getHostAddress(), port, soTimeout);
        in = new DataInputStream(new BufferedInputStream(
                socket.getInputStream()));
        out = new DataOutputStream(new BufferedOutputStream(
                socket.getOutputStream(), BUF_SIZE));
        isOpen = true;
    }



    /**
     * Close the session, as described in the Session interface.  Any exception
     * that occurs in the process is logged, but otherwise ignored.
     */
    public void close() {
        inUse = true;
        isOpen = false;
        closeSocket();
        inUse = false;
    }


    private void closeSocket() {
        try {
            in.close();
        } catch (IOException ioe) {
            userLogger.warning(ioe.getMessage());
        }

        try {
            out.close();
        } catch (IOException ioe) {
            userLogger.warning(ioe.getMessage());
        }

        try {
            socket.close();
        } catch (IOException ioe) {
            userLogger.warning(ioe.getMessage());
        }
    }

    /**
     * Receive data from the peer. This method is unsynchronized; the caller
     * MUST provide synchronization against other calls to read.
     */
    public String read() throws IOException {
        debugLogger.finest("enter");
        try {
            int n = readSize();
            debugLogger.finer("PDU size: " + n);
            String data = readData(n);
            supportLogger.info(data);
            return data;
        } catch (java.net.SocketTimeoutException ste) {
            userLogger.severe(ste.getMessage());
            userLogger.severe("epp.session.read.timeout");
            throw ste;
        } catch (IOException ioe) {
            userLogger.severe(ioe.getMessage());
            throw ioe;
        } finally {
            debugLogger.finest("exit");
        }
    }


    /**
     * Send data to peer. This method is unsynchronized; the caller MUST
     * provide synchronization against other calls to <code>write(String)</code>.
     */
    public void write(String xml) throws IOException {
        if (!isOpen()) {
            try {
                open();
            } catch (SessionOpenException e) {
                e.printStackTrace();
            }
        }
        doWrite(xml);
    }

    private void doWrite(String xml) throws IOException {
        debugLogger.finest("enter");

        if (out == null) {
            throw new UninitialisedSessionException();
        }

        try {
            final byte[] xmlBytes = xml.getBytes();
            writeSize(xmlBytes.length);
            writeData(xmlBytes);
        } catch (IOException ioe) {
            isInvalid = true;
            throw ioe;
        }

        debugLogger.finest("exit");
    }



    private int readSize() throws IOException {
        return in.readInt() - 4;
    }

    private void writeSize(int size) throws IOException {
        out.writeInt(size + 4);
    }

    private String readData(int length) throws IOException {
        byte[] inputBuffer = new byte[length];
        in.readFully(inputBuffer, 0, length);

        return new String(inputBuffer);
    }

    private void writeData(final byte[] xml) throws IOException {
        out.write(xml);
        out.flush();
    }



    /**
     * See the description of isAvailable in the Session interface.
     */
    public boolean isAvailable() {
        return (isOpen && !inUse);
    }


    /**
     * See the description of release in the Session interface.
     */
    public void release() {
        debugLogger.finest("enter");
        synchronized (this) {
            inUse = false;
            notify();
        }
        debugLogger.finest("exit");
    }
}
